#!/usr/bin/env python
#
# Copyright 2009 The Closure Library Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS-IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Utility for Closure Library dependency calculation.

ClosureBuilder scans source files to build dependency info.  From the
dependencies, the script can produce a deps.js file, a manifest in dependency
order, a concatenated script, or compiled output from the Closure Compiler.

Paths to files can be expressed as individual arguments to the tool (intended
for use with find and xargs).  As a convenience, --root can be used to specify
all JS files below a directory.

usage: %prog [options] [file1.js file2.js ...]
"""




import logging
import optparse
import os
import sys

import depstree
import source
import sourcedeps
import jsdocdeps


def _GetOptionsParser():
  """Get the options parser."""

  parser = optparse.OptionParser(__doc__)
  parser.add_option('-n',
                    '--namespace',
                    dest='namespaces',
                    action='append',
                    default=[],
                    help='One or more namespaces to calculate dependencies '
                    'for.  These namespaces will be combined with those given '
                    'with the -i flag to form the set of namespaces to find '
                    'dependencies for.  A Closure namespace is a '
                    'dot-delimited path expression declared with a call to '
                    'goog.provide() (e.g. "goog.array" or "foo.bar").')

  parser.add_option('--closure_pass',
                    dest='closure_pass',
                    default=False,
                    action='store_true',
                    help='Closure pass')

  parser.add_option('--dojo_pass',
                    dest='dojo_pass',
                    default=False,
                    action='store_true',
                    help='Dojo pass')

  parser.add_option('--jsdoc_pass',
                    dest='jsdoc_pass',
                    default=False,
                    action='store_true',
                    help='JsDoc pass')

  return parser


def _GetClosureBaseFile(sources):
  """Given a set of sources, returns the one base.js file.

  Note that if zero or two or more base.js files are found, an error message
  will be written and the program will be exited.

  Args:
    sources: An iterable of _PathSource objects.

  Returns:
    The _PathSource representing the base Closure file.
  """
  filtered_base_files = filter(_IsClosureBaseFile, sources)
  if not filtered_base_files:
    logging.error('No Closure base.js file found.')
    sys.exit(1)
  if len(filtered_base_files) > 1:
    logging.error('More than one Closure base.js files found at these paths:')
    for base_file in filtered_base_files:
      logging.error(base_file.GetPath())
    sys.exit(1)
  return filtered_base_files[0]


def _IsClosureBaseFile(js_source):
  """Returns true if the given _PathSource is the Closure base.js source."""
  if os.path.basename(js_source.GetPath()) == 'base.js':
    # Sanity check that this is the Closure base file.  Check that this
    # is where goog is defined.
    for line in js_source.GetSource().splitlines():
      if line.startswith('var goog = goog || {};'):
        return True
  return False


class _PathSource(source.Source):
  """Source file subclass that remembers its file path."""

  def __init__(self, provides, requires, path):
    """Initialize a source.

    Args:
      path: str, Path to a JavaScript file.  The source string will be read
        from this file.
    """
    super(_PathSource, self).__init__(provides, requires)
    self.path = path


def _GetSource(path, passes):
  provides = set()
  requires = set()

  for source_pass in passes:
    f = open(path)
    pass_provides, pass_requires = source_pass(f.read())
    f.close()
    
    provides.update(pass_provides)
    requires.update(pass_requires)

  return _PathSource(provides, requires, path)


def main():
  logging.basicConfig(format=(sys.argv[0] + ': %(message)s'),
                      level=logging.INFO)
  options, args = _GetOptionsParser().parse_args()

  sources = []

  passes = []
  if options.closure_pass:
    passes.append(sourcedeps.GetClosureDependencies)

  if options.dojo_pass:
    passes.append(sourcedeps.GetDojoDependencies)

  if options.jsdoc_pass:
    passes.append(jsdocdeps.GetJsDocDependencies)

  # TODO(nnaze): Add jsdeps and other passes.
    
  for js_path in args:
    logging.info('Scanning %s' % js_path)
    sources.append(_GetSource(js_path, passes))

  logging.info('%s sources scanned.', len(sources))

  # Though deps output doesn't need to query the tree, we still build it
  # to validate dependencies.
  logging.info('Building dependency tree..')
  tree = depstree.DepsTree(sources)

  input_namespaces = set(options.namespaces)

  if not input_namespaces:
    logging.error('At least one namespace must be '
                  'specified with the --namespace or flags.')
    sys.exit(2)

  # The Closure Library base file must go first.

  deps = tree.GetDependencies(input_namespaces)

  for dep in deps:
    print dep.path


if __name__ == '__main__':
  main()
